/**
 * @file gmail.c
 * GMail source for TrayInfo.
 */

/* GMail support added by Laszlo Ashin <kodest@gmail.com> */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h> /* close() */
#include <netdb.h>

#include "trayinfo.h"
#include "utils.h"

#include <openssl/ssl.h>
#include <openssl/rand.h>

/** Source handle. */
static source_handle_t *gmail_sh;

/** Host name of the google mail server to connect to. */
#define GMAILHOST	"mail.google.com"

/** Port number we want to connect to. */
#define GMAILPORT	"443"

/** Size of the recieve buffer. */
#define RECVBUFSIZE	512

/** Size of the send buffer. */
#define SENDBUFSIZE	512

/** Number of mails currently waiting for us in our google mail box. */
static int nmails = 0;

static SSL *ssl = NULL;
static SSL_CTX *ctx = NULL;

/**
 * Authentication string containing base64 encoded string of the username and
 * the password delimited by a colon.
 */
static char *auth;

/** Shows the number of mails waiting for us. */
static int
gmail_write(double now, int fd)
{
	if (!nmails) return 0;
	return show("gmail:%d", nmails);
}

/** Opens a connection to the google mail server */
static int
gmail_open()
{
	struct addrinfo hints;
	struct addrinfo *res, *rp;
	int fd, s;
	char buf[SENDBUFSIZE];
	int n;

	memset(&hints, 0, sizeof(hints));
	hints.ai_flags = AI_ADDRCONFIG;
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_protocol = 0;

	s = getaddrinfo(GMAILHOST, GMAILPORT, &hints, &res);
	if (s) {
		fprintf(stderr, "gmail: getaddrinfo(): %s\n", gai_strerror(s));
		return 0;
	}

	for (rp = res; rp; rp = rp->ai_next) {
		fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
		if (fd == -1)
			continue;

		if (!connect(fd, rp->ai_addr, rp->ai_addrlen))
			break;

		close(fd);
	}

	freeaddrinfo(res);

	if (!rp) {
		fprintf(stderr, "gmail: unable to connect\n");
		return 0;
	}

	/* TODO: does SSL context need to be opened at a higher level? */
	ctx = SSL_CTX_new(SSLv23_client_method());
	if (!ctx) {
		fprintf(stderr, "gmail: failed to create ssl context\n");
		goto err_clo;
	}

	ssl = SSL_new(ctx);
	if (!ssl) {
		fprintf(stderr, "gmail: failed to initialize ssl layer\n");
		goto err_ctx;
	}

	if (!SSL_set_fd(ssl, fd)) {
		fprintf(stderr, "gmail: ssl association failed\n");
		goto err_ssl;
	}

	RAND_poll();
	while (!RAND_status()) {
		unsigned short r = rand() & 0xffff;
		RAND_seed(&r, sizeof(r));
	}

	if (SSL_connect(ssl) != 1) {
		fprintf(stderr, "gmail: failed to make ssl connection\n");
		goto err_ssl;
	}

	/* Assemble the payload. */
	n = snprintf(
		buf, sizeof(buf),
		"GET /mail/feed/atom HTTP/1.0\r\n"
		"Authorization: Basic %s\r\n"
		"Host: "GMAILHOST":"GMAILPORT"\r\n"
		"Connection: Close\r\n"
		"\r\n",
		auth
	);

	if (n < 0 || n >= sizeof(buf)) {
		fprintf(stderr, "gmail: sendbuf was too small\n");
		goto err_ssl;
	}

	SSL_write(ssl, buf, n);

	return fd;

err_ssl:
	SSL_free(ssl);
	ssl = NULL;
err_ctx:
	SSL_CTX_free(ctx);
	ctx = NULL;
err_clo:
	close(fd);
	return 0;
}

/** Close the connection. */
static void
gmail_close(int fd)
{
	if (ssl) SSL_shutdown(ssl);
	close(fd);
	if (ssl) SSL_free(ssl);
	ssl = NULL;
	if (ctx) SSL_CTX_free(ctx);
	ctx = NULL;
}

/** Process the received xml data searching for number of unread messages. */
static void
gmail_process_recved(char *s)
{
	if (!strncmp(s, "fullcount>", 10)) {
		if (!sscanf(s + 10, "%d", &nmails))
			nmails = 0;
	}
}

/** Read data google mail server sent to us. */
static ssize_t
gmail_read(int fd)
{
	static char buf[RECVBUFSIZE];
	static size_t sord = 0; /* size of received data */
	ssize_t n = 0;
	char *sep;
	
	if (!ssl) return 0;
	memset(buf, 0, sizeof(buf));
	n = SSL_read(ssl, buf + sord, sizeof(buf) - sord);
	if (n <= 0) return n;
	sord += n;
	while ((sep = memchr(buf, '<', sord))) {
		int l;

		*sep++ = '\0';
		l = sep - buf;
		gmail_process_recved(buf);
		sord -= l;
		memmove(buf, buf + l, sord);
	}
	/* check for overflow */
	if (sord == sizeof(buf)) {
		/* simply drop everything */
		sord = 0;
	}
	return n;
}

static int
gmail_init(source_handle_t *sh)
{
	auth = NULL;
	return 0;
}

static void
gmail_done(void)
{
	if (auth) free(auth);
	auth = NULL;
}

/**
 * Processes the authentication string read from the configuration file.
 * @param s Authentication string read from the configuration file.
 */
static int
gmail_cfg_auth(const char *s)
{
	if (auth) free(auth);
	auth = strdup(s);

	return 0;
}

/**
 * Generates authentication string by asking the user. This function is
 * invoked by the default config, when no authentication string is set by
 * the user yet.
 */
static int
gmail_generate_auth(const char *dummy)
{
	char user[80], pass[80];
	char *s, *realm;
	size_t len, realmlen;

	request_quit();
	printf("Enter GMail username: ");
	fflush(stdout);
	if (fgets(user, sizeof(user), stdin) != user) {
		fprintf(stderr, "no username specified.\n");
		return !0;
	}
	for (s = user + strlen(user) - 1; s >= user && *s < 33; --s);
	s[1] = '\0';
	printf("Enter GMail password (won't be echoed): ");
	fflush(stdout);
	if (!get_pass(pass, sizeof(pass))) {
		fprintf(stderr, "no password specified.\n");
		return !0;
	}

	/* compute http basic realm */
	len = strlen(user) + strlen(pass) + 2;
	s = (char *)malloc(len);
	strcpy(s, user);
	strcat(s, ":");
	strcat(s, pass);
	realm = base64_encode(s, len - 1);
	realmlen = strlen(realm);
	puts("You can add the following line to your trayinfo config file:");
	printf("gmail-auth %s\n", realm);
	puts("");
	puts("Please remove the following line if occurs in the config file:");
	puts("gmail-generate-auth");
	puts("");

	/* wipe all */
	memset(realm, 0, realmlen);
	free(realm);
	realmlen = 0;
	memset(s, 0, len);
	free(s);
	len = 0;
	memset(user, 0, sizeof(user));
	memset(pass, 0, sizeof(pass));

	return 0;
}

int
init_module(module_phase_t phase)
{
	switch (phase) {
	case MODULE_PHASE_INIT: {
		source_t source = {
			.name = "gmail",
			.init = gmail_init,
			.done = gmail_done,
			.write = gmail_write,
			.open = gmail_open,
			.close = gmail_close,
			.read = gmail_read,
			.wait_retry = 5 * 60,
		};
		gmail_sh = register_source(&source);
		if (gmail_sh == NULL) {
			return !0;
		}
		register_config_handler("gmail-auth", gmail_cfg_auth);
		register_config_handler("gmail-generate-auth", gmail_generate_auth);
		break;
	}
	case MODULE_PHASE_EXIT:
		forget_config_handler("gmail-generate-auth", gmail_generate_auth);
		forget_config_handler("gmail-auth", gmail_cfg_auth);
		forget_source(gmail_sh);
		break;
	}
	return 0;
}
